/**
 * ExtDirect4Java
 * @author Maxim Egorov
 * @version 0.2-alpha
 *
 * Copyright(c) 2009, Swissdotnet S�rl
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 */
package ch.swissdotnet.extdirect4j;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

abstract class AbstractAction extends RemoteObject {
    private Class clazz = null;
    private Object instance = null;
    private boolean lazyInit = false;
    private final Map<String, Map<Integer, ExtDirectMethod>> methodMap = new HashMap<String, Map<Integer, ExtDirectMethod>>();

    abstract void beforeApply();

    abstract class MethodResolver {
        abstract ExtDirectMethod resolve(Method method);
    }

    AbstractAction(final Object instance) {
        this(instance.getClass());
        this.instance = instance;
    }

    AbstractAction(final Class clazz) {
        super(clazz.getName());
        this.clazz = clazz;
    }

    AbstractAction(final String className) {
        super();
        this.clazz = Utils.getClassByName(className);
        this.setRemoteName(this.clazz.getName());
    }

    Class getClazz() {
        return clazz;
    }

    boolean isLazyInit() {
        return lazyInit;
    }

    boolean isInstantiated() {
        return this.instance != null;
    }

    void setLazyInit(final boolean lazyInit) {
        this.lazyInit = lazyInit;
    }

    public Map<String, Map<Integer, ExtDirectMethod>> getMethodMap() {
        return this.methodMap;
    }

    void init() {
        try {
            this.instance = this.clazz.newInstance();
        } catch (Exception e) {
            throw new ExtDirectException("error while instantiation class " + clazz.getCanonicalName() + " object", e);
        }
    }

    private ExtDirectMethod lookupMethod(final String methodName, final int argNum) {
        Map<Integer, ExtDirectMethod> methods = this.methodMap.get(methodName);
        if (methods != null) {
            return methods.get(argNum);
        } else {
            return null;
        }
    }

    public Class[] getParametersType(final String methodName, final int argNum) {
        ExtDirectMethod extDirectMethod = this.lookupMethod(methodName, argNum);
        if (extDirectMethod != null) {
            return extDirectMethod.getMethod().getParameterTypes();
        } else {
            return null;
        }
    }

    public Object invoke(String methodName, List args) throws InvocationTargetException, IllegalAccessException {
        final int argNum = args.size();
        final ExtDirectMethod method = lookupMethod(methodName, argNum);
        if (method == null) {
            throw new ExtDirectException("requested method '" + methodName + "' with requested number of argument(s):"
                    + argNum + " is not found");
        }
        if (!this.isInstantiated()) {
            this.init();
        }
        return method.getMethod().invoke(this.instance, args.toArray());
    }

    private void applyMethod(ExtDirectMethod extMethod) {
        final Method method = extMethod.getMethod();
        final String remoteName = extMethod.getRemoteName();
        final int argNum = method.getParameterTypes().length;

        this.checkIfPublic(method);
        this.checkIfExistWithSameArguments(remoteName, argNum);

        Map<Integer, ExtDirectMethod> extMethods = this.methodMap.get(remoteName);
        if (extMethods == null) {
            extMethods = new HashMap<Integer, ExtDirectMethod>();
            this.methodMap.put(remoteName, extMethods);
        }
        extMethods.put(argNum, extMethod);
    }

    void applyMethodsRecursively(final Class clazz, final MethodResolver resolver) {
        if (clazz != null) {
            this.applyMethodsRecursively(clazz.getSuperclass(), resolver);
            final Method[] methodsArr = clazz.getDeclaredMethods();
            for (Method method : methodsArr) {
                if (!Modifier.isAbstract(method.getModifiers())) {
                    ExtDirectMethod extDirectMethod = resolver.resolve(method);
                    if (extDirectMethod != null) {
                        this.applyMethod(extDirectMethod);
                    }
                }
            }
        }
    }

    private void checkIfPublic(final Method method) {
        if (!Modifier.isPublic(method.getModifiers())) {
            throw new ExtDirectException("specified method: " + this.clazz.getCanonicalName() + "#"
                    + method.getName() + " must be declared as public");
        }
    }

    private void checkIfExistWithSameArguments(final String remoteName, final int argNum) {
        if (this.lookupMethod(remoteName, argNum) != null) {
            throw new ExtDirectException("detected more than one method with name:" + remoteName
                    + " and same number of arguments: " + argNum + " in inheritance tree of class: " + this.clazz);
        }
    }
}
